﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace MadMediaManager.Core.Util
{
    /// <summary>
    /// Helper class that aids in the constrcution and manipulation of Linq <see cref="Expression"/>s.
    /// </summary>
    public static class ExpressionUtil
    {
        public static String PropertyName<T, TProperty>(Expression<Func<T, TProperty>> expression)
        {
            return PropertyName(expression as LambdaExpression);
        }

        public static String PropertyName(LambdaExpression expression)
        {
            String property = expression.Body.ToString();
            Int32 firstPeriod = property.IndexOf(".") + 1;

            if (expression.Body.NodeType == ExpressionType.MemberAccess)
            {
                return property.Substring(firstPeriod, property.Length - firstPeriod);
            }

            //for properties that are members of other objects i.e. DateTime.Now.Date.Second
            if (expression.Body.NodeType == ExpressionType.Convert)
            {
                String subString = property.Substring(firstPeriod, property.Length - (1 + firstPeriod));
                return subString.Contains('.') ? subString.Substring(0, subString.IndexOf('.')) : subString;
            }

            throw new NotSupportedException(expression.Body.NodeType + " is not supported by this method.");
        }

        public static String PropertyName<T>(Expression<Func<T, object>> expression)
        {
            return PropertyName(expression as LambdaExpression);
        }

        #region OrderBy //pulled from stackoverflow

        public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string property)
        {
            return ApplyOrder(source, property, "OrderBy");
        }
        public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> source, string property)
        {
            return ApplyOrder(source, property, "OrderByDescending");
        }
        public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source, string property)
        {
            return ApplyOrder(source, property, "ThenBy");
        }
        public static IOrderedQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> source, string property)
        {
            return ApplyOrder(source, property, "ThenByDescending");
        }
        private static IOrderedQueryable<T> ApplyOrder<T>(IQueryable<T> source, string property, string methodName)
        {
            var pair = GetParameterExpression(typeof(T), property);
            Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), pair.Value.Key);
            LambdaExpression lambda = Expression.Lambda(delegateType, pair.Value.Value, pair.Key);
            object result = typeof(Queryable).GetMethods().Single(
                method => method.Name == methodName
                    && method.IsGenericMethodDefinition
                    && method.GetGenericArguments().Length == 2
                    && method.GetParameters().Length == 2)
                    .MakeGenericMethod(typeof(T), pair.Value.Key)
                    .Invoke(null, new object[] { source, lambda });
            return (IOrderedQueryable<T>)result;
        }

        #endregion OrderBy

        #region Support methods

        /// <summary>
        /// Build a <see cref="ConstantExpression"/> from a value and property type.  <paramref name="propertyValue"/> will be
        /// cast to the type of <paramref name="propertyType"/> if it is a <see cref="string"/>.
        /// </summary>
        /// <exception cref="InvalidCastException">Thrown if the case from <see cref="string"/> to <paramref name="propertyType"/>
        /// fails.
        /// </exception>
        /// <param name="propertyType">The type of the property</param>
        /// <param name="propertyValue">The value of the property</param>
        /// <returns></returns>
        public static ConstantExpression GetConstantExpression(Type propertyType, object propertyValue)
        {
            ConstantExpression constant;
            if (propertyValue == null)
            {
                constant = Expression.Constant(null);
            }
            else
            {
                Type propertyValueType;
                if (!propertyType.Equals(typeof(string)) && propertyValue is string)
                {
                    if (propertyType.IsEnum)
                    {
                        propertyValue = Enum.Parse(propertyType, propertyValue as String, true);
                        propertyValueType = propertyType;
                    }
                    else if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
                    {
                        var nullableType = propertyType.GetGenericArguments()[0];
                        propertyValueType = typeof(Nullable<>).MakeGenericType(nullableType);
                        propertyValue = nullableType.IsEnum
                                       ? Enum.Parse(nullableType, propertyValue as string)
                                       : Convert.ChangeType(propertyValue, nullableType);
                    }
                    else
                    {
                        propertyValue = Convert.ChangeType(propertyValue, propertyType);
                        propertyValueType = propertyType;
                    }
                }
                else
                {
                    propertyValueType = propertyValue.GetType();
                }
                constant = Expression.Constant(propertyValue, propertyValueType);
            }

            return constant;
        }

        /// <summary>
        /// Gets a key value pair indicating the parameter expression, the property expression of the parameter and the type of that property.
        /// </summary>
        /// <param name="type">The soruce type "T"</param>
        /// <param name="propertyName">The name of the property on "T"</param>
        /// <returns></returns>
        public static KeyValuePair<ParameterExpression, KeyValuePair<Type, Expression>> GetParameterExpression(
            Type type, String propertyName)
        {
            string[] props = propertyName.Split('.');
            ParameterExpression arg = Expression.Parameter(type, "x");
            Expression expr = arg;
            Type propertyType = type;
            foreach (string prop in props)
            {
                PropertyInfo pi = propertyType.GetProperty(prop);
                expr = Expression.Property(expr, pi);
                propertyType = pi.PropertyType;
            }

            return new KeyValuePair<ParameterExpression, KeyValuePair<Type, Expression>>(
                arg, new KeyValuePair<Type, Expression>(propertyType, expr));
        }

        #endregion Support Methods

        #region Where Expressions

        public static Expression<Func<T, bool>> WhereEqual<T>(String propertyName, Object propertyValue)
        {
            var pair = GetParameterExpression(typeof(T), propertyName);
            ConstantExpression con = GetConstantExpression(pair.Value.Key, propertyValue);
            BinaryExpression be = Expression.Equal(pair.Value.Value, con);
            return Expression.Lambda<Func<T, Boolean>>(be, pair.Key);
        }

        public static Expression<Func<T, bool>> WhereNotEqual<T>(String propertyName, Object propertyValue)
        {
            var pair = GetParameterExpression(typeof(T), propertyName);
            ConstantExpression con = GetConstantExpression(pair.Value.Key, propertyValue);
            BinaryExpression be = Expression.NotEqual(pair.Value.Value, con);
            return Expression.Lambda<Func<T, Boolean>>(be, pair.Key);
        }

        public static Expression<Func<T, bool>> WhereGreaterThan<T>(String propertyName, Object propertyValue)
        {
            var pair = GetParameterExpression(typeof(T), propertyName);
            ConstantExpression con = GetConstantExpression(pair.Value.Key, propertyValue);
            BinaryExpression be = Expression.GreaterThan(pair.Value.Value, con);
            return Expression.Lambda<Func<T, Boolean>>(be, pair.Key);
        }

        public static Expression<Func<T, bool>> WhereGreaterThanOrEqual<T>(String propertyName, Object propertyValue)
        {
            var pair = GetParameterExpression(typeof(T), propertyName);
            ConstantExpression con = GetConstantExpression(pair.Value.Key, propertyValue);
            BinaryExpression be = Expression.GreaterThanOrEqual(pair.Value.Value, con);
            return Expression.Lambda<Func<T, Boolean>>(be, pair.Key);
        }

        public static Expression<Func<T, bool>> WhereLessThan<T>(String propertyName, Object propertyValue)
        {
            var pair = GetParameterExpression(typeof(T), propertyName);
            ConstantExpression con = GetConstantExpression(pair.Value.Key, propertyValue);
            BinaryExpression be = Expression.LessThan(pair.Value.Value, con);
            return Expression.Lambda<Func<T, Boolean>>(be, pair.Key);
        }

        public static Expression<Func<T, bool>> WhereLessThanOrEqual<T>(String propertyName, Object propertyValue)
        {
            var pair = GetParameterExpression(typeof(T), propertyName);
            ConstantExpression con = GetConstantExpression(pair.Value.Key, propertyValue);
            BinaryExpression be = Expression.LessThanOrEqual(pair.Value.Value, con);
            return Expression.Lambda<Func<T, Boolean>>(be, pair.Key);
        }

        enum MatchMode { Contains, StartsWith, EndsWith }
        private static Expression<Func<T, Boolean>> GetLikeExpression<T>(String propertyName, Object propertyValue,
            MatchMode mode, Boolean not)
        {
            var pair = GetParameterExpression(typeof(T), propertyName);
            ConstantExpression con = GetConstantExpression(pair.Value.Key, propertyValue);
            MethodInfo info;
            switch (mode)
            {
                case MatchMode.Contains:
                    info = typeof(String).GetMethod("Contains", new[] { typeof(String) });
                    break;
                case MatchMode.EndsWith:
                    info = typeof(String).GetMethod("StartsWith", new[] { typeof(String) });
                    break;
                case MatchMode.StartsWith:
                    info = typeof(String).GetMethod("EndsWith", new[] { typeof(String) });
                    break;
                default:
                    throw new ArgumentException(string.Format("'{0}' is not supported.", mode), "mode");
            }

            Expression e = Expression.Call(pair.Value.Value, info, con);
            return Expression.Lambda<Func<T, Boolean>>(not ? Expression.Not(e) : e, pair.Key);
        }

        public static Expression<Func<T, bool>> WhereLike<T>(String propertyName, Object propertyValue)
        {
            return GetLikeExpression<T>(propertyName, propertyValue, MatchMode.Contains, false);
        }

        public static Expression<Func<T, bool>> WhereNotLike<T>(String propertyName, Object propertyValue)
        {
            return GetLikeExpression<T>(propertyName, propertyValue, MatchMode.Contains, true);
        }

        public static Expression<Func<T, bool>> WhereStartsWith<T>(String propertyName, Object propertyValue)
        {
            return GetLikeExpression<T>(propertyName, propertyValue, MatchMode.StartsWith, false);
        }

        public static Expression<Func<T, bool>> WhereNotStartsWith<T>(String propertyName, Object propertyValue)
        {
            return GetLikeExpression<T>(propertyName, propertyValue, MatchMode.StartsWith, true);
        }

        public static Expression<Func<T, bool>> WhereEndsWith<T>(String propertyName, Object propertyValue)
        {
            return GetLikeExpression<T>(propertyName, propertyValue, MatchMode.EndsWith, false);
        }

        public static Expression<Func<T, bool>> WhereNotEndsWith<T>(String propertyName, Object propertyValue)
        {
            return GetLikeExpression<T>(propertyName, propertyValue, MatchMode.EndsWith, true);
        }

        /// <summary>
        /// Allows for chaining Where expressions together as Or conditions.
        /// Pulled from http://www.albahari.com/nutshell/predicatebuilder.aspx.  
        /// If <paramref name="sourceExpression"/> is null, then <paramref name="expressionToAppend"/> is returned as the expression.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="sourceExpression"></param>
        /// <param name="expressionToAppend"></param>
        /// <returns></returns>
        public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> sourceExpression,
                                                      Expression<Func<T, bool>> expressionToAppend)
        {
            if (sourceExpression != null)
            {
                var invokedExpr = Expression.Invoke(expressionToAppend, sourceExpression.Parameters);
                return Expression.Lambda<Func<T, bool>>
                    (Expression.OrElse(sourceExpression.Body, invokedExpr), sourceExpression.Parameters);
            }
            return expressionToAppend;
        }

        /// <summary>
        /// Allows for chaining Where expressions together as And conditions.
        /// Pulled from http://www.albahari.com/nutshell/predicatebuilder.aspx.  
        /// If <paramref name="sourceExpression"/> is null, then <paramref name="expressionToAppend"/> is returned as the expression.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="sourceExpression"></param>
        /// <param name="expressionToAppend"></param>
        /// <returns></returns>
        public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> sourceExpression,
                                                             Expression<Func<T, bool>> expressionToAppend)
        {
            if (sourceExpression != null)
            {
                var invokedExpr = Expression.Invoke(expressionToAppend, sourceExpression.Parameters);
                return Expression.Lambda<Func<T, bool>>
                    (Expression.AndAlso(sourceExpression.Body, invokedExpr), sourceExpression.Parameters);
            }
            return expressionToAppend;
        }

        #endregion Where Expressions
        
        public static String MethodName<T>(Expression<Action<T>> expression)
        {
            if (expression.Body.NodeType == ExpressionType.Call)
            {
                String methodName = expression.Body.ToString();
                Int32 firstPeriod = methodName.IndexOf(".") + 1;
                Int32 openPara = methodName.IndexOf("(");
                return methodName.Substring(firstPeriod, openPara - firstPeriod);
            }

            throw new NotSupportedException(expression.Body.NodeType + " is not supported by this method.");
        }

        public static String MethodName<T>(Expression<Func<T>> expression)
        {
            if (expression.Body.NodeType == ExpressionType.Call)
            {
                String methodName = expression.Body.ToString();
                Int32 firstPeriod = methodName.IndexOf(".") + 1;
                Int32 openPara = methodName.IndexOf("(");
                return methodName.Substring(firstPeriod, openPara - firstPeriod);
            }

            throw new NotSupportedException(expression.Body.NodeType + " is not supported by this method.");
        }
    }
}
